package me.yonatan.maven.plugin;

/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Update .classpath and .project files to have GAE nature, GAE builders. Adds
 * GAE to the classpath, and sets it above the maven's as recommanded at
 * Google's <a href=
 * "http://googlewebtoolkit.blogspot.com/2010/08/how-to-use-google-plugin-for-eclipse.html"
 * >blog entry</a>
 * 
 * @goal update
 * 
 * @phase validate
 */
public class EclipseGaeMojo extends AbstractMojo {

	private String CLASSPATH_FILE = ".classpath";
	private String PROJECT_FILE = ".project";

	private static final String CLASSPATH_ROOT = "classpath";
	private static final String CLASSPATHENTRY_NAME = "classpathentry";
	private static final String GAE_CONTAINER_PATH_KEY = "com.google.appengine.eclipse.core.GAE_CONTAINER";
	private static final String MAVEN2_CLASSPATH_KEY = "org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER";

	private static final String GAE_NATURE_KEY = "com.google.appengine.eclipse.core.gaeNature";
	private static final String PROJECT_ROOT = "projectDescription";
	private static final String BUILDSPEC_NODE = "buildSpec";
	private static final String NATURES_NODE = "natures";
	private static final String NATURE_NODE = "nature";

	private static final String WEBAPP_VALIDATOR = "com.google.gdt.eclipse.core.webAppProjectValidator";
	private static final String ENHENCER_BUILDER = "com.google.appengine.eclipse.core.enhancerbuilder";
	private static final String PROJECT_VALIDATOR = "com.google.appengine.eclipse.core.projectValidator";

	private static final String BUILDERCOMMAND_NODE = "buildCommand";
	private static final String NAME_NODE = "name";
	private static final String ARGUMENTS_NODE = "arguments";

	private boolean testMode = false;

	protected void setTestMode(boolean testMode) {
		this.testMode = testMode;
	}

	protected boolean getTestMode() {
		return testMode;
	}

	protected void setClasspathFile(String classpathFile) {
		this.CLASSPATH_FILE = classpathFile;
	}

	protected String getClasspathFile() {
		return CLASSPATH_FILE;
	}

	protected void setProjectFile(String projectFile) {
		this.PROJECT_FILE = projectFile;
	}

	protected String getProjectFile() {
		return PROJECT_FILE;
	}

	/**
	 * Location of the eclipse's project .class and .project files
	 * 
	 * @parameter expression="${project.basedir}"
	 * @required
	 */
	private File projectDirectory;

	/**
	 * Fail if eclispe files are missing (.project and .classpath)
	 * 
	 * @parameter expression=java.lang.Boolean
	 * 
	 */
	private boolean failOnMissingFiles = true;

	public void execute() throws MojoExecutionException, MojoFailureException {
		Log log = getLog();
		File f = projectDirectory;
		if (f == null) {
			throw new MojoFailureException(
					"Porject base directory wasn't defined");
		}
		if (!f.exists()) {
			String message = "Could not find directory " + f.getAbsolutePath();
			if (failOnMissingFiles)
				throw new MojoFailureException(message);
			log.warn(message);
			return;
		}

		String path = f.getAbsolutePath();
		File classpath = new File(path, CLASSPATH_FILE);
		File project = new File(path, PROJECT_FILE);

		if (!classpath.exists()) {
			String message = "Could not find file "
					+ classpath.getAbsolutePath();
			if (failOnMissingFiles)
				throw new MojoFailureException(message);
			log.warn(message);
			return;
		}

		if (!project.exists()) {
			String message = "Could not find file " + project.getAbsolutePath();
			if (failOnMissingFiles)
				throw new MojoFailureException(message);
			log.warn(message);
			return;
		}

		Document classpathDom = getDom(classpath);
		Document projectDom = getDom(project);

		manipulateClasspath(classpathDom);
		manipulateProject(projectDom);

		writeDomToFile(classpathDom, classpath);
		writeDomToFile(projectDom, project);
	}

	void writeDomToFile(Document dom, File file) throws MojoFailureException,
			MojoExecutionException {
		if (testMode) {
			file = new File(file.getAbsolutePath().concat(".new"));
		}
		try {
			FileUtils.writeStringToFile(file, domToString(dom));
		} catch (IOException e) {
			throw new MojoFailureException("Could not write file "
					+ file.getAbsolutePath() + " - " + e.getMessage());
		}
	}

	private void manipulateProject(Document dom) throws MojoFailureException,
			MojoExecutionException {
		Log log = getLog();
		if (log.isDebugEnabled())
			log.debug("Before manipulation " + domToString(dom));
		NodeList buildspecs = dom.getElementsByTagName(BUILDSPEC_NODE);
		if (buildspecs.getLength() != 1) {
			throw new MojoFailureException("Bad " + PROJECT_FILE
					+ " - too many or too few '" + BUILDSPEC_NODE + "'");
		}

		NodeList natures = dom.getElementsByTagName(NATURE_NODE);

		boolean found = false;
		for (int i = 0; i < natures.getLength(); i++) {
			Node nature = natures.item(i);
			if (GAE_NATURE_KEY.equals(nature.getTextContent())) {
				found = true;
				break;
			}
		}

		if (!found) {
			// Set GAE nature
			Node gaeNature = dom.createElement(NATURE_NODE);
			gaeNature.setTextContent(GAE_NATURE_KEY);

			NodeList naturesNodes = dom.getElementsByTagName(NATURES_NODE);
			if (naturesNodes.getLength() != 1) {
				throw new MojoFailureException("Bad " + PROJECT_FILE
						+ " - too many or too few '" + NATURES_NODE + "'");
			}
			Node naturesNode = naturesNodes.item(0);
			naturesNode.appendChild(gaeNature);

			// Add builders
			Element buildSpec = (Element) buildspecs.item(0);
			buildSpec.appendChild(createBuilderCommand(dom, WEBAPP_VALIDATOR));
			buildSpec.appendChild(createBuilderCommand(dom, ENHENCER_BUILDER));
			buildSpec.appendChild(createBuilderCommand(dom, PROJECT_VALIDATOR));
		}
		if (log.isDebugEnabled())
			log.debug("After manipulation " + domToString(dom));
	}

	private Node createBuilderCommand(Document dom, String name) {
		Node builderCommand = dom.createElement(BUILDERCOMMAND_NODE);
		Node arguments = dom.createElement(ARGUMENTS_NODE);
		Node nameNode = dom.createElement(NAME_NODE);
		nameNode.setTextContent(name);
		builderCommand.appendChild(nameNode);
		builderCommand.appendChild(arguments);
		return builderCommand;
	}

	private void manipulateClasspath(Document dom) throws MojoFailureException,
			MojoExecutionException {
		Log log = getLog();
		if (log.isDebugEnabled())
			log.debug("Before manipulation:\n" + domToString(dom));
		Node root = dom.getFirstChild();
		if (!CLASSPATH_ROOT.equals(root.getNodeName())) {
			throw new MojoFailureException("Bad " + CLASSPATH_FILE
					+ " - root is not '" + CLASSPATH_ROOT + "'");
		}
		NodeList children = root.getChildNodes();

		Node gaeCpe = null;
		Node mvnCpe = null;

		int gaeCpeIdx = -1;
		int mvnCpeIdx = -1;
		for (int i = 0; i < children.getLength(); i++) {
			Node cpEntry = children.item(i);
			if (CLASSPATHENTRY_NAME.equals(cpEntry.getNodeName())) {
				String path = cpEntry.getAttributes().getNamedItem("path")
						.getNodeValue();
				log.debug("Classpath entry " + i + " with path " + path);
				if (mvnCpe == null && MAVEN2_CLASSPATH_KEY.equals(path)) {
					mvnCpeIdx = i;
					log.debug("Maven2 classpath entry at " + i);
					mvnCpe = cpEntry;
				} else if (gaeCpe == null
						&& GAE_CONTAINER_PATH_KEY.equals(path)) {
					mvnCpeIdx = i;
					log.debug("GAE classpath entry at " + i);
					gaeCpe = cpEntry;
				}
			}
		}
		if (gaeCpe == null) {
			log.debug("No GAE classpath entry found");
			gaeCpe = dom.createElement(CLASSPATHENTRY_NAME);
			NamedNodeMap attrs = gaeCpe.getAttributes();
			Attr kind = dom.createAttribute("kind");
			kind.setValue("con");
			Attr path = dom.createAttribute("path");
			path.setValue(GAE_CONTAINER_PATH_KEY);
			attrs.setNamedItem(kind);
			attrs.setNamedItem(path);
		}
		if (mvnCpe != null) {
			if (gaeCpeIdx > -1)
				root.removeChild(gaeCpe);
			root.insertBefore(gaeCpe, mvnCpe);
		} else {
			if (gaeCpeIdx == -1)
				root.appendChild(gaeCpe);
		}
		if (log.isDebugEnabled())
			log.debug("After manipulation:\n" + domToString(dom));
	}

	private String domToString(Node dom) throws MojoExecutionException {
		TransformerFactory transfac = TransformerFactory.newInstance();
		Transformer trans;
		try {
			trans = transfac.newTransformer();
		} catch (TransformerConfigurationException e) {
			throw new MojoExecutionException("", e);
		}
		trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
		trans.setOutputProperty(OutputKeys.INDENT, "yes");

		// create string from xml tree
		StringWriter sw = new StringWriter();
		StreamResult result = new StreamResult(sw);
		DOMSource source = new DOMSource(dom);
		try {
			trans.transform(source, result);
		} catch (TransformerException e) {
			throw new MojoExecutionException("", e);
		}
		String xmlString = sw.toString();
		return xmlString;
	}

	private Document getDom(File file) throws MojoFailureException,
			MojoExecutionException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder db = dbf.newDocumentBuilder();

			return db.parse(file);
		} catch (ParserConfigurationException e) {
			throw new MojoFailureException(e, "Can't parse xml file "
					+ file.getAbsolutePath(), e.getMessage());
		} catch (SAXException e) {
			throw new MojoFailureException(e, "Can't parse xml file "
					+ file.getAbsolutePath(), e.getMessage());
		} catch (IOException e) {
			throw new MojoFailureException(e, "Can't parse xml file "
					+ file.getAbsolutePath(), e.getMessage());
		} catch (Exception e) {
			throw new MojoExecutionException("Can't parse xml file "
					+ file.getAbsolutePath(), e);
		}
	}
}
